#region Namespaces

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

#endregion

namespace Vibstudio.X4NET.Xml.Linq
{
    [XmlSchemaProvider(null, IsAny = true)]
    public class XElement : XContainer, IXmlSerializable
    {
        private static readonly IEnumerable<XElement> emptySequence = new List<XElement>();

        private bool _explicitIsEmpty = true;

        private XName _name;

        public XElement(XName name, object content)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            _name = name;
            Add(content);
        }

        public XElement(XElement other)
        {
            if (other == null)
            {
                throw new ArgumentNullException("other");
            }
            _name = other._name;
            Add(other.Attributes());
            Add(other.Nodes());
        }

        public XElement(XName name)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            _name = name;
        }

        public XElement(XName name, params object[] content)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            _name = name;
            Add(content);
        }

        public XElement(XStreamingElement other)
        {
            if (other == null)
            {
                throw new ArgumentNullException("other");
            }
            _name = other.Name;
            Add(other.Contents);
        }

        public static IEnumerable<XElement> EmptySequence
        {
            get { return emptySequence; }
        }

        public XAttribute FirstAttribute { get; internal set; }

        public XAttribute LastAttribute { get; internal set; }

        public bool HasAttributes
        {
            get { return FirstAttribute != null; }
        }

        public bool HasElements
        {
            get
            {
                foreach (object o in Nodes())
                {
                    if (o is XElement)
                    {
                        return true;
                    }
                }
                return false;
            }
        }

        public bool IsEmpty
        {
            get { return !Nodes().GetEnumerator().MoveNext() && _explicitIsEmpty; }
            internal set { _explicitIsEmpty = value; }
        }

        public XName Name
        {
            get { return _name; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentException("Name");
                }
                OnNameChanging(this);
                _name = value;
                OnNameChanged(this);
            }
        }

        public override XmlNodeType NodeType
        {
            get { return XmlNodeType.Element; }
        }

        public string Value
        {
            get
            {
                StringBuilder sb = null;
                foreach (XNode n in Nodes())
                {
                    if (sb == null)
                    {
                        sb = new StringBuilder();
                    }
                    if (n is XText)
                    {
                        sb.Append(((XText)n).Value);
                    }
                    else if (n is XElement)
                    {
                        sb.Append(((XElement)n).Value);
                    }
                }
                return sb == null ? String.Empty : sb.ToString();
            }
            set
            {
                RemoveNodes();
                Add(value);
            }
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            Save(writer);
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            ReadContentFrom(reader, LoadOptions.None);
        }

        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }

        //private static object ConvertForAssignment(object value)
        //{
        //    var node = value as XmlNode;
        //    if (node == null)
        //    {
        //        return value;
        //    }
        //    var doc = new XmlDocument();
        //    doc.AppendChild(doc.ImportNode(node, true));
        //    return Parse(doc.InnerXml);
        //}

        [CLSCompliant(false)]
        public static explicit operator bool(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XUtil.ConvertToBoolean(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator bool?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (bool?)null : XUtil.ConvertToBoolean(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator DateTime(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XUtil.ToDateTime(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator DateTime?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (DateTime?)null : XUtil.ToDateTime(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator DateTimeOffset(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToDateTimeOffset(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator DateTimeOffset?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (DateTimeOffset?)null : XmlConvert.ToDateTimeOffset(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator decimal(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToDecimal(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator decimal?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (decimal?)null : XmlConvert.ToDecimal(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator double(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToDouble(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator double?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (double?)null : XmlConvert.ToDouble(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator float(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToSingle(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator float?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (float?)null : XmlConvert.ToSingle(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator Guid(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToGuid(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator Guid?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (Guid?)null : XmlConvert.ToGuid(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator int(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToInt32(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator int?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (int?)null : XmlConvert.ToInt32(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator long(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToInt64(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator long?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (long?)null : XmlConvert.ToInt64(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator uint(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToUInt32(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator uint?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (uint?)null : XmlConvert.ToUInt32(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator ulong(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToUInt64(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator ulong?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (ulong?)null : XmlConvert.ToUInt64(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator TimeSpan(XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return XmlConvert.ToTimeSpan(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator TimeSpan?(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value == null ? (TimeSpan?)null : XmlConvert.ToTimeSpan(element.Value);
        }

        [CLSCompliant(false)]
        public static explicit operator string(XElement element)
        {
            if (element == null)
            {
                return null;
            }

            return element.Value;
        }

        private IEnumerable<XElement> GetAncestorList(XName name, bool getMeIn)
        {
            List<XElement> list = new List<XElement>();
            if (getMeIn)
            {
                list.Add(this);
            }
            for (XElement el = Parent as XElement; el != null; el = el.Parent as XElement)
            {
                if (name == null || el.Name == name)
                {
                    list.Add(el);
                }
            }
            return list;
        }

        public XAttribute Attribute(XName name)
        {
            XAttribute next;
            for (XAttribute a = FirstAttribute; a != null; a = a.NextAttribute)
            {
                if (a.Name == name)
                {
                    return a;
                }
            }
            return null;
        }

        public IEnumerable<XAttribute> Attributes()
        {
            XAttribute next;
            for (XAttribute a = FirstAttribute; a != null; a = next)
            {
                next = a.NextAttribute;
                yield return a;
            }
        }

        // huh?
        public IEnumerable<XAttribute> Attributes(XName name)
        {
            foreach (XAttribute a in Attributes())
            {
                if (a.Name == name)
                {
                    yield return a;
                }
            }
        }

        private static void DefineDefaultSettings(XmlReaderSettings settings, LoadOptions options)
        {
            settings.ProhibitDtd = false;

            settings.IgnoreWhitespace = (options & LoadOptions.PreserveWhitespace) == 0;
        }

        private static XmlReaderSettings CreateDefaultSettings(LoadOptions options)
        {
            var settings = new XmlReaderSettings();
            DefineDefaultSettings(settings, options);
            return settings;
        }

        public static XElement Load(string uri)
        {
            return Load(uri, LoadOptions.None);
        }

        public static XElement Load(string uri, LoadOptions options)
        {
            XmlReaderSettings s = CreateDefaultSettings(options);

            using (XmlReader r = XmlReader.Create(uri, s))
            {
                return LoadCore(r, options);
            }
        }

        public static XElement Load(TextReader textReader)
        {
            return Load(textReader, LoadOptions.None);
        }

        public static XElement Load(TextReader textReader, LoadOptions options)
        {
            XmlReaderSettings s = CreateDefaultSettings(options);

            using (XmlReader r = XmlReader.Create(textReader, s))
            {
                return LoadCore(r, options);
            }
        }

        public static XElement Load(XmlReader reader)
        {
            return Load(reader, LoadOptions.None);
        }

        public static XElement Load(XmlReader reader, LoadOptions options)
        {
            XmlReaderSettings s = reader.Settings != null ? reader.Settings.Clone() : new XmlReaderSettings();
            DefineDefaultSettings(s, options);

            using (XmlReader r = XmlReader.Create(reader, s))
            {
                return LoadCore(r, options);
            }
        }

        internal static XElement LoadCore(XmlReader r, LoadOptions options)
        {
            r.MoveToContent();
            if (r.NodeType != XmlNodeType.Element)
            {
                throw new InvalidOperationException("The XmlReader must be positioned at an element");
            }
            XName name = XName.Get(r.LocalName, r.NamespaceURI);
            XElement e = new XElement(name);
            e.FillLineInfoAndBaseUri(r, options);

            if (r.MoveToFirstAttribute())
            {
                do
                {
                    if (r.LocalName == "xmlns" && r.NamespaceURI == XNamespace.Xmlns.NamespaceName)
                    {
                        e.SetAttributeValue(XNamespace.None.GetName("xmlns"), r.Value);
                    }
                    else
                    {
                        e.SetAttributeValue(XName.Get(r.LocalName, r.NamespaceURI), r.Value);
                    }
                    e.LastAttribute.FillLineInfoAndBaseUri(r, options);
                }
                while (r.MoveToNextAttribute());
                r.MoveToElement();
            }
            if (!r.IsEmptyElement)
            {
                r.Read();
                e.ReadContentFrom(r, options);
                r.ReadEndElement();
                e._explicitIsEmpty = false;
            }
            else
            {
                e._explicitIsEmpty = true;
                r.Read();
            }
            return e;
        }

        public static XElement Parse(string text)
        {
            return Parse(text, LoadOptions.None);
        }

        public static XElement Parse(string text, LoadOptions options)
        {
            return Load(new StringReader(text), options);
        }

        public void RemoveAll()
        {
            RemoveAttributes();
            RemoveNodes();
        }

        public void RemoveAttributes()
        {
            while (FirstAttribute != null)
            {
                LastAttribute.Remove();
            }
        }

        public void Save(string fileName)
        {
            Save(fileName, SaveOptions.None);
        }

        public void Save(string fileName, SaveOptions options)
        {
            XmlWriterSettings s = new XmlWriterSettings();

            if ((options & SaveOptions.DisableFormatting) == SaveOptions.None)
            {
                s.Indent = true;
            }

            using (XmlWriter w = XmlWriter.Create(fileName, s))
            {
                Save(w);
            }
        }

        public void Save(TextWriter textWriter)
        {
            Save(textWriter, SaveOptions.None);
        }

        public void Save(TextWriter textWriter, SaveOptions options)
        {
            XmlWriterSettings s = new XmlWriterSettings();

            if ((options & SaveOptions.DisableFormatting) == SaveOptions.None)
            {
                s.Indent = true;
            }

            using (XmlWriter w = XmlWriter.Create(textWriter, s))
            {
                Save(w);
            }
        }

        public void Save(XmlWriter writer)
        {
            WriteTo(writer);
        }

        public IEnumerable<XElement> AncestorsAndSelf()
        {
            return GetAncestorList(null, true);
        }

        public IEnumerable<XElement> AncestorsAndSelf(XName name)
        {
            return GetAncestorList(name, true);
        }

        public IEnumerable<XElement> DescendantsAndSelf()
        {
            List<XElement> list = new List<XElement>();
            list.Add(this);
            list.AddRange(Descendants());
            return list;
        }

        public IEnumerable<XElement> DescendantsAndSelf(XName name)
        {
            List<XElement> list = new List<XElement>();
            if (name == this._name)
            {
                list.Add(this);
            }
            list.AddRange(Descendants(name));
            return list;
        }

        public IEnumerable<XNode> DescendantNodesAndSelf()
        {
            yield return this;
            foreach (XNode node in DescendantNodes())
            {
                yield return node;
            }
        }

        public void SetAttributeValue(XName name, object value)
        {
            XAttribute a = Attribute(name);
            if (value == null)
            {
                if (a != null)
                {
                    a.Remove();
                }
            }
            else
            {
                if (a == null)
                {
                    SetAttributeObject(new XAttribute(name, value));
                }
                else
                {
                    a.Value = XUtil.ToString(value);
                }
            }
        }

        private void SetAttributeObject(XAttribute a)
        {
            OnAddingObject(a);
            a = (XAttribute)XUtil.GetDetachedObject(a);
            a.SetOwner(this);
            if (FirstAttribute == null)
            {
                FirstAttribute = a;
                LastAttribute = a;
            }
            else
            {
                LastAttribute.NextAttribute = a;
                a.PreviousAttribute = LastAttribute;
                LastAttribute = a;
            }
            OnAddedObject(a);
        }

        private string LookupPrefix(string ns, XmlWriter w)
        {
            string prefix = ns.Length > 0 ? GetPrefixOfNamespace(ns) ?? w.LookupPrefix(ns) : String.Empty;
            foreach (XAttribute a in Attributes())
            {
                if (a.IsNamespaceDeclaration && a.Value == ns)
                {
                    if (a.Name.Namespace == XNamespace.Xmlns)
                    {
                        prefix = a.Name.LocalName;
                    }

                    break;
                }
            }
            return prefix;
        }

        private static string CreateDummyNamespace(ref int createdNS, IEnumerable<XAttribute> atts, bool isAttr)
        {
            if (!isAttr && atts.All(a => a.Name.LocalName != "xmlns" || a.Name.NamespaceName == XNamespace.Xmlns.NamespaceName))
            {
                return String.Empty;
            }
            string p = null;
            do
            {
                p = "p" + (++createdNS);
                if (atts.All(a => a.Name.LocalName != p || a.Name.NamespaceName == XNamespace.Xmlns.NamespaceName))
                {
                    break;
                }
            }
            while (true);
            return p;
        }

        public override void WriteTo(XmlWriter writer)
        {
            string prefix = LookupPrefix(_name.NamespaceName, writer);
            int createdNS = 0;
            if (prefix == null)
            {
                prefix = CreateDummyNamespace(ref createdNS, Attributes(), false);
            }

            writer.WriteStartElement(prefix, _name.LocalName, _name.Namespace.NamespaceName);

            foreach (XAttribute a in Attributes())
            {
                if (a.IsNamespaceDeclaration)
                {
                    if (a.Name.Namespace == XNamespace.Xmlns)
                    {
                        writer.WriteAttributeString("xmlns", a.Name.LocalName, XNamespace.Xmlns.NamespaceName, a.Value);
                    }
                    else
                    {
                        writer.WriteAttributeString("xmlns", a.Value);
                    }
                }
                else
                {
                    string apfix = LookupPrefix(a.Name.NamespaceName, writer);
                    if (apfix == null)
                    {
                        apfix = CreateDummyNamespace(ref createdNS, Attributes(), true);
                    }
                    writer.WriteAttributeString(apfix, a.Name.LocalName, a.Name.Namespace.NamespaceName, a.Value);
                }
            }

            foreach (XNode node in Nodes())
            {
                node.WriteTo(writer);
            }

            if (_explicitIsEmpty)
            {
                writer.WriteEndElement();
            }
            else
            {
                writer.WriteFullEndElement();
            }
        }

        public XNamespace GetDefaultNamespace()
        {
            for (XElement el = this; el != null; el = el.Parent)
            {
                foreach (XAttribute a in el.Attributes())
                {
                    if (a.IsNamespaceDeclaration && a.Name.Namespace == XNamespace.None)
                    {
                        return XNamespace.Get(a.Value);
                    }
                }
            }
            return XNamespace.None;
        }

        public XNamespace GetNamespaceOfPrefix(string prefix)
        {
            for (XElement el = this; el != null; el = el.Parent)
            {
                foreach (XAttribute a in el.Attributes())
                {
                    if (a.IsNamespaceDeclaration &&
                        (prefix.Length == 0 && a.Name.LocalName == "xmlns" || a.Name.LocalName == prefix))
                    {
                        return XNamespace.Get(a.Value);
                    }
                }
            }
            return XNamespace.None;
        }

        public string GetPrefixOfNamespace(XNamespace ns)
        {
            foreach (string prefix in GetPrefixOfNamespaceCore(ns))
            {
                if (GetNamespaceOfPrefix(prefix) == ns)
                {
                    return prefix;
                }
            }
            return null;
        }

        private IEnumerable<string> GetPrefixOfNamespaceCore(XNamespace ns)
        {
            for (XElement el = this; el != null; el = el.Parent)
            {
                foreach (XAttribute a in el.Attributes())
                {
                    if (a.IsNamespaceDeclaration && a.Value == ns.NamespaceName)
                    {
                        yield return a.Name.Namespace == XNamespace.None ? String.Empty : a.Name.LocalName;
                    }
                }
            }
        }

        public void ReplaceAll(object content)
        {
            ReplaceAll(XUtil.ExpandArray(content).ToArray());
        }

        public void ReplaceAll(params object[] content)
        {
            RemoveNodes();
            Add(content);
        }

        public void ReplaceAttributes(object content)
        {
            ReplaceAttributes(XUtil.ExpandArray(content).ToArray());
        }

        public void ReplaceAttributes(params object[] content)
        {
            RemoveAttributes();
            Add(content);
        }

        public void SetElementValue(XName name, object value)
        {
            var element = Element(name);
            if (element == null)
            {
                if (value != null)
                {
                    Add(new XElement(name, value));
                }
            }
            else if (value == null)
            {
                element.Remove();
            }
            else
            {
                element.SetValue(value);
            }
        }

        public void SetValue(object value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            if (value is XAttribute || value is XDocument || value is XDeclaration || value is XDocumentType)
            {
                throw new ArgumentException(String.Format("Node type {0} is not allowed as element value",
                                                          value.GetType()));
            }
            RemoveNodes();
            foreach (object o in XUtil.ExpandArray(value))
            {
                Add(o);
            }
        }

        internal override bool OnAddingObject(object o, bool rejectAttribute, XNode refNode, bool addFirst)
        {
            if (o is XDocument || o is XDocumentType || o is XDeclaration || (rejectAttribute && o is XAttribute))
            {
                throw new ArgumentException(String.Format("A node of type {0} cannot be added as a content", o.GetType()));
            }

            XAttribute a = o as XAttribute;
            if (a != null)
            {
                foreach (XAttribute ia in Attributes())
                {
                    if (a.Name == ia.Name)
                    {
                        throw new InvalidOperationException(String.Format("Duplicate attribute: {0}", a.Name));
                    }
                }
                SetAttributeObject(a);
                return true;
            }
            else if (o is string && refNode is XText)
            {
                ((XText)refNode).Value += o as string;
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}